package server.processors;

import java.nio.channels.ClosedChannelException;
import java.util.concurrent.ScheduledFuture;

import server.NetworkMediator;
import server.ServerStateMediator;
import server.schedulables.ScheduledAuctionExpiration;

import common.Auction;
import common.AuctionMatch;
import common.AuctionResponse;
import common.Bid;
import common.PhaseEnum;
import common.Player;
import common.Round;
import common.Sendable;

public class AuctionProcessor extends Processor {

	public AuctionProcessor(){}
	
	public AuctionProcessor( NetworkMediator mediator, ServerStateMediator server ) {
		super( mediator, server );
	}
	
	@Override
	public Processor getInstanceOf(NetworkMediator mediator,
			ServerStateMediator server) {
		return new AuctionProcessor( mediator, server );
	}

	@Override
	public boolean isInterestedIn(Sendable s) {
		return s instanceof Auction;
	}

	public void run() {
		ServerStateMediator server = getServer();
		NetworkMediator network = getNetwork();
		
		Player player = server.sentBy();
		
		Round round = player.getRound();
		
		if( round == null )
			return;
		
		//Auctions are only useful in the trading phase
		if( round.getCurrentPhase() != PhaseEnum.TRADING )
			return;
		
		Auction auction = (Auction) server.getMessage(); //Guaranteed to work because of isInterestedIn
		
		if( !player.ownsAuction( auction ) )
			return;
		
		auction.generateID();
		
		if( !sendAuctionAcknowledgement( player, auction ) )
			return;
		
		//Start the auction matching process
		
		
		boolean isBuyer = auction instanceof Bid;
		
		while( true ) {
			
			//Auctions are only useful in the trading phase
			if( round.getCurrentPhase() != PhaseEnum.TRADING )
				return;
			
			Auction matched = null;
			Player matchedPlayer = null;
			
			for( Player p : round.getPlayers() ) {
				//Find the best match against every player.
				Auction currentMatch = p.match( auction,  player );
				
				if( currentMatch == null )
					continue;
				
//				System.out.println("Auction Matching: " + p.getName() + " " + currentMatch );
//				System.out.println("Auction: " + currentMatch.price + " " + currentMatch.quantity );
				
				if( matched == null ) {
					matched = currentMatch;
					matchedPlayer = p;
				} else if( ( isBuyer && matched.getMatchPrice( auction ) > currentMatch.getMatchPrice( auction ) ) ||
						 (!isBuyer && matched.getMatchPrice( auction ) < currentMatch.getMatchPrice( auction ) ) ) {
					//Update for the best match in the case of a buyer or seller
					matched = currentMatch;
					matchedPlayer = p;
				}
					
			}
			
			
			if( matched == null ) {
				// If no matched auctions were found, give up and let subsequent auctions try to match with it.
				
				//Make sure the remove and add can be done in one atomic action.
				synchronized( player ) {
				// Must schedule this unmatched Auction with an expiration event.
					if( player.removeAuction( auction ) ) {
						if( auction.setScheduled() ) {
							ScheduledFuture<?> future = network.schedule( new ScheduledAuctionExpiration( network, auction, player ), auction.duration, auction.time );
							auction.setScheduledFuture( future );
							
							player.addAuction( auction );
						}
					}
				}
				
				return; 
			} else {
				// If the matched auction was removed from the matched player it means this buyer/seller was successfully matched
				// broadcast to both players the auction match
				
				
				Player firstLock = player.getLarger( matchedPlayer );
				Player secondLock = ( player.equals( firstLock ) ) ? matchedPlayer : player;
				
				
				//Acquire the locks in a ordered fashion to prevent deadlocking
				//This also ensures if there is any failure in acquiring one of the "chopsticks"
				//that it can be safely returned.
				synchronized( firstLock ) {
					synchronized( secondLock ) {
						
						boolean acquiredMatched = matchedPlayer.removeAuction( matched );
						boolean reacquiredMyOwn = player.removeAuction( auction );
						
						if( acquiredMatched && reacquiredMyOwn ) {
							//If able to acquire both auctions, send out the auction match.
							successfulAuction( player, auction, matchedPlayer, matched );
							return;
						} else if( acquiredMatched && !reacquiredMyOwn ) {
							//If unable to reacquire my own auction it means it was paired with someone else
							// return the matched auction back to the matched player.
							matchedPlayer.addAuction( matched );
							
							//Restart the search as the match you acquired because it may require rematching.
							player = matchedPlayer;
							auction = matched;
							continue;
						} else if( !acquiredMatched && reacquiredMyOwn ) {
							//Restart search since someone stole my matched auction :*(
							player.addAuction( auction );
							continue;
						} else {
							// unable to acquire either so give up
							return;
						}
					}
				}
				
				
				
			}
			
			// If unable to acquire the best match auction it means that someone else did before hand, restart the search.
		}
		
	
	}
	
	private boolean sendAuctionAcknowledgement( Player from, Auction a ) {
		NetworkMediator network = getNetwork();
		
		try {
			
			//Must synchronize on from to prevent other threads from stealing this auction before its broadcasted.
			synchronized( from ) {
				
				AuctionResponse response = new AuctionResponse( a, from.getLimits( a.commodity ), !from.canAllowAndAdd(a) );
				
				//If the auction itself has an error or the player's cumulative auction result in an error
				// only send an AuctionResponse to that player and no one else.
				if( response.hasError() ) {
					network.sendTo( from, response );
					return false;
				} else {
					network.sendTo( from, response );
					network.sendToAllInRound( from.getRound(), a );
					return true;
				}
			}
				
		} catch (ClosedChannelException e) {
			e.printStackTrace();
		}
		
		return false;
	}
	
	private void successfulAuction( Player from, Auction a1, Player to, Auction a2 ) {
//		System.out.println("Succesful Auction: " + from.getName() + " and " + to.getName() );
		NetworkMediator network = getNetwork();
		
		//Update the auction status of both players (updating their scores too)
		from.buyOrSell( a1, a2 );
		to.buyOrSell( a2, a1 );
		
		//Cancel the expiration events for these two auctions.
		a1.cancelExpiration();
		a2.cancelExpiration();
		
		
		//Send auction match packet to all players.
		try {
			network.sendToAllInRound(from.getRound(), new AuctionMatch( a1, a2 ) );
		} catch (ClosedChannelException e) {
			e.printStackTrace();
		}
		
	}

}
